package com.gitee.zycra.file.utils;

import com.github.luben.zstd.Zstd;
import com.github.luben.zstd.ZstdInputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

/**
 * Util for compress directory and file.
 *
 * @author zycra
 * @since 1.0.0
 */
public final class CompressUtil {

    private static final Logger LOGGER = LoggerFactory.getLogger(CompressUtil.class);

    /**
     * Buffer size for read and write file.
     *
     * @since 1.0.0
     */
    private static final int BUF_SIZE = 4 * 1024 * 1024;

    /**
     * Zstd write file buffer size.
     *
     * @since 1.0.0
     */
    private static final int ZSTD_OUT_BUF_SIZE = BUF_SIZE + 1024;

    /**
     * Read and write auth.
     *
     * @since 1.0.0
     */
    private static final String RW = "rw";

    /**
     * Read auth.
     *
     * @since 1.0.0
     */
    private static final String R = "r";

    /**
     * Zstd compress level.
     *
     * @since 1.0.0
     */
    private static final int ZSTD_COMPRESS_LEVEL = 3;

    private CompressUtil() {
    }

    /**
     * zip compress file or directory.
     *
     * @param originFile the file or directory to zip compress.
     * @param zipFile    the path to store zip compressed file.
     * @return whether successful.
     * @since 1.0.0
     */
    public static boolean zipCompress(File originFile, File zipFile) {
        if (!Files.isReadable(originFile.toPath()) || zipFile == null) {
            return false;
        }
        if (!mkdirs(zipFile.getParentFile())) {
            return false;
        }
        try (FileOutputStream fos = new FileOutputStream(zipFile);
             ZipOutputStream zos = new ZipOutputStream(fos)) {
            String parentPath = originFile.getParent();
            parentPath = parentPath.endsWith(File.separator) ? parentPath : parentPath + File.separator;
            return zipDirectory(parentPath, originFile, zos);
        } catch (Exception e) {
            LOGGER.error("zip compress error, originFile={}, zipFile={}", originFile.getAbsolutePath(), zipFile.getAbsolutePath(), e);
            return false;
        }
    }

    /**
     * Zip compress a directory.
     *
     * @param parentPath the parent path of target directory.
     * @param file       the target directory to compress.
     * @param zos        {#link ZipOutputStream} instance.
     * @return whether successful.
     * @throws IOException compress exception.
     * @since 1.0.0
     */
    private static boolean zipDirectory(String parentPath, File file, ZipOutputStream zos) throws IOException {
        if (!file.isDirectory()) {
            return zipFile(parentPath, file, zos);
        }
        File[] files = file.listFiles();
        if (files == null || files.length == 0) {
            zos.putNextEntry(new ZipEntry(file.getPath().replace(parentPath, "") + "/"));
            zos.closeEntry();
            return true;
        }
        for (File fileName : files) {
            boolean singleSuccess = zipDirectory(parentPath, fileName, zos);
            if (!singleSuccess) {
                return false;
            }
        }
        return true;
    }

    /**
     * Zip compress a file.
     *
     * @param parentPath the parent path of target file.
     * @param file       the target file to compress.
     * @param zos        {#link ZipOutputStream} instance.
     * @return whether successful.
     * @throws IOException compress exception.
     * @since 1.0.0
     */
    private static boolean zipFile(String parentPath, File file, ZipOutputStream zos) throws IOException {
        try (FileInputStream fis = new FileInputStream(file)) {
            zos.putNextEntry(new ZipEntry(file.getPath().replace(parentPath, "")));
            int r;
            byte[] buf = new byte[BUF_SIZE];
            while ((r = fis.read(buf)) > 0) {
                zos.write(buf, 0, r);
            }
            zos.closeEntry();
            return true;
        }
    }

    /**
     * Zip uncompress a file.
     *
     * @param zipFile          zip file to uncompress.
     * @param unCompressFolder the directory to store uncompressed files.
     * @return whether successful.
     * @since 1.0.0
     */
    public static boolean zipUnCompress(File zipFile, File unCompressFolder) {
        if (!Files.isReadable(zipFile.toPath()) || !zipFile.isFile()) {
            return false;
        }
        if (!mkdirs(unCompressFolder)) {
            return false;
        }
        try (FileInputStream fis = new FileInputStream(zipFile);
             BufferedInputStream bis = new BufferedInputStream(fis);
             ZipInputStream zis = new ZipInputStream(bis)) {
            ZipEntry zipEntry;
            String parentPath = unCompressFolder.getAbsolutePath();
            parentPath = parentPath.endsWith(File.separator) ? parentPath : parentPath + File.separator;
            while ((zipEntry = zis.getNextEntry()) != null) {
                File file = new File(parentPath + zipEntry.getName());
                if (zipEntry.isDirectory()) {
                    if (!mkdirs(file)) {
                        return false;
                    }
                    continue;
                }
                if (!mkdirs(file.getParentFile())) {
                    return false;
                }
                try (FileOutputStream fos = new FileOutputStream(file)) {
                    int r;
                    byte[] buf = new byte[BUF_SIZE];
                    while ((r = zis.read(buf)) > 0) {
                        fos.write(buf, 0, r);
                    }
                    fos.flush();
                    zis.closeEntry();
                }
            }
            return true;
        } catch (Exception e) {
            LOGGER.error("zip uncompress error zipFile={}, unCompressFolder={}", zipFile.getAbsolutePath(), unCompressFolder.getAbsolutePath(), e);
        }
        return false;
    }

    /**
     * Zstd compress a file.
     *
     * @param originFile the file to compress.
     * @param zstdFile   the path to store zstd compressed file.
     * @return whether successful.
     * @since 1.0.0
     */
    public static boolean zstdCompress(File originFile, File zstdFile) {
        if (!Files.isReadable(originFile.toPath()) || !originFile.isFile() || zstdFile == null) {
            return false;
        }
        if (!mkdirs(zstdFile.getParentFile())) {
            return false;
        }
        ByteBuffer inBuffer = ByteBuffer.allocateDirect(BUF_SIZE);
        ByteBuffer outBuffer = ByteBuffer.allocateDirect(ZSTD_OUT_BUF_SIZE);
        try (RandomAccessFile inFile = new RandomAccessFile(originFile, R);
             RandomAccessFile outFile = new RandomAccessFile(zstdFile, RW);
             FileChannel inChannel = inFile.getChannel();
             FileChannel outChannel = outFile.getChannel()) {
            while (inChannel.read(inBuffer) > 0) {
                inBuffer.flip();
                long compressedSize = Zstd.compressDirectByteBuffer(outBuffer, 0, outBuffer.capacity(),
                        inBuffer, 0, inBuffer.limit(), ZSTD_COMPRESS_LEVEL);
                outBuffer.position((int) compressedSize);
                outBuffer.flip();
                outChannel.write(outBuffer);
                inBuffer.clear();
                outBuffer.clear();
            }
            return true;
        } catch (Exception e) {
            LOGGER.error("zstd compress error, originFile={}", originFile.getAbsolutePath(), e);
        }
        return false;
    }

    /**
     * Zstd uncompress a file.
     *
     * @param zstdFile   the file to uncompress.
     * @param targetFile the path to store zstd uncompressed file.
     * @return whether successful.
     * @since 1.0.0
     */
    public static boolean zstdUnCompress(File zstdFile, File targetFile) {
        if (!Files.isReadable(zstdFile.toPath()) || !zstdFile.isFile() || targetFile == null) {
            return false;
        }
        if (!mkdirs(targetFile.getParentFile())) {
            return false;
        }
        try (FileInputStream fis = new FileInputStream(zstdFile);
             ZstdInputStream zis = new ZstdInputStream(fis);
             FileOutputStream fos = new FileOutputStream(targetFile)) {
            byte[] buf = new byte[BUF_SIZE];
            int r;
            while ((r = zis.read(buf)) > 0) {
                fos.write(buf, 0, r);
            }
            fos.flush();
            return true;
        } catch (Exception e) {
            LOGGER.error("zstd uncompress error, zstdFile={}", zstdFile.getAbsolutePath(), e);
        }
        return false;
    }

    /**
     * Create directorys.
     *
     * @param file file object.
     * @return whether successful.
     * @since 1.0.0
     */
    public static boolean mkdirs(File file) {
        try {
            Files.createDirectories(file.toPath());
            return true;
        } catch (IOException e) {
            LOGGER.error("create folder error, path={}", file.getPath(), e);
        }
        return false;
    }
}
